#include "Model.hpp"
#include <iostream>
#include <cstdlib>
#include <string>
#include <algorithm>
#include "glpp/Shader.hpp"
#include "glpp/UniformBlock.hpp"

#define NUM_BONES_PER_VERTEX 4
struct VertexBoneData
{
	unsigned int ids[NUM_BONES_PER_VERTEX];
	float weights[NUM_BONES_PER_VERTEX];
};

static const char *vertexShaderCode =
GLSL_330
(
	layout (std140) uniform Matrices
	{
		mat4 proj;
		mat4 view;
		mat4 model;
	};

	layout (location = 0) in vec3 position;
	layout (location = 1) in vec3 normal;
	layout (location = 2) in vec2 texCoord;
	layout (location = 3) in ivec4 boneIDs;
	layout (location = 4) in vec4 weights;

	out vec4 vertexPos;
	out vec2 TexCoord;
	out vec3 Normal;

	const int MAX_BONES = 100;

	uniform mat4 bones[MAX_BONES];

	void main()
	{
		mat4 boneTransform = bones[boneIDs[0]] * weights[0];
		boneTransform += bones[boneIDs[1]] * weights[1];
		boneTransform += bones[boneIDs[2]] * weights[2];
		boneTransform += bones[boneIDs[3]] * weights[3];

		Normal = normalize(vec3(view * model * vec4(normal, 0.0)));
		TexCoord = texCoord;
		gl_Position = proj * view * model * vec4(position, 1.0);
	}
);

static const char *fragmentShaderCode =
GLSL_330
(
	layout (std140) uniform Material
	{
		vec4 diffuse;
		vec4 ambient;
		vec4 specular;
		vec4 emissive;
		float shininess;
		int texCount;
	};
	
	uniform sampler2D texUnit;

	in vec3 Normal;
	in vec2 TexCoord;

	out vec4 outputColor;

	void main()
	{
		vec4 color;
		vec4 amb;
		float intensity;
		vec3 lightDir;
		vec3 n;

		lightDir = normalize( vec3(1.0, 1.0, 1.0) );
		n = normalize(Normal);
		intensity = max(dot(lightDir, n), 0.0);
		if(intensity < 0.99)
		{
			if(intensity >= 0.4)
				intensity = 0.8;
			else
				intensity = 0.4;
		}

		if(texCount == 0)
		{
			color = diffuse;
			amb = ambient;
		}
		else
		{
			color = texture2D(texUnit, TexCoord);
			amb = color * 0.33;
		}
		outputColor = vec4((color.rgb * intensity) + amb.rgb, color.a);
	}
);

/*
static const char *vertexShaderCode =
GLSL_330
(
	layout(std140) uniform Matrices
	{
		mat4 proj;
		mat4 view;
		mat4 model;
	};

	in vec3 position;
	in vec3 normal;
	in vec2 texCoord;

	out vec3 position_eye;
	out vec3 normal_eye;
	out vec2 TexCoord;

	void main()
	{
		position_eye = vec3(view * model * vec4(position, 1.0));
		normal_eye = normalize(vec3(view * model * vec4(normal, 0.0)));
		TexCoord = texCoord;
		gl_Position = proj * vec4(position_eye, 1.0);
	}
);

static const char *fragmentShaderCode =
GLSL_330
(
	layout(std140) uniform Material
	{
		vec4 diffuse;
		vec4 ambient;
		vec4 specular;
		vec4 emissive;
		float shininess;
		int texCount;
	};

	uniform mat4 view;
	uniform sampler2D texUnit;

	in vec3 position_eye;
	in vec3 normal_eye;
	in vec2 TexCoord;
	out vec4 outColor;

	vec3 light_position_world = vec3(0.0, -20.0, 60.0);
	vec3 Ls = vec3(0.7, 0.7, 0.7);
	vec3 Ld = vec3(0.7, 0.7, 0.7);
	vec3 La = vec3(0.3, 0.3, 0.28);

	vec3 Ks = vec3(0.1, 0.1, 0.1);
	vec3 Kd = vec3(1.0, 0.8980, 0.7843);
	vec3 Ka = vec3(1.0, 0.8980, 0.7843);
	float specular_exponent = 100.0;

	void main()
	{
		vec3 ne = normalize(normal_eye);
		vec3 Ia = La * Ka;

		vec3 light_position_eye = vec3(view * vec4(light_position_world, 1.0));
		vec3 distance_to_light_eye = light_position_eye - position_eye;
		vec3 direction_to_light_eye = normalize(distance_to_light_eye);

		float dot_prod = dot(direction_to_light_eye, normal_eye);
		dot_prod = max(dot_prod, 0.0);
		vec3 Id = Ld * Kd * dot_prod;

		vec3 reflection_eye = reflect(-direction_to_light_eye, normal_eye);
		vec3 cameraPosition = -transpose(mat3(view)) * view[3].xyz;
		vec3 surface_to_viewer_eye = normalize(cameraPosition - position_eye);
		float dot_prod_specular = dot(reflection_eye, surface_to_viewer_eye);
		dot_prod_specular = max(dot_prod_specular, 0.0);
		float specular_factor = pow(dot_prod_specular, specular_exponent);
		vec3 Is = Ls * Ks * specular_factor;

		outColor = vec4(Is + Id + Ia, 1.0);
		//outColor = vec4(Id, 1.0);
	}
);
*/
struct ModelShader
{
	ModelShader() :
		shaderObject(gl::Shader::loadFromMemory(vertexShaderCode, fragmentShaderCode))
	{
		shaderObject.bindUniformBlock("Matrices", matricesBlock);
		shaderObject.bindUniformBlock("Material", materialBlock);
	}
	gl::Shader shaderObject;
	gl::UniformBlock matricesBlock, materialBlock;
};

static ModelShader *modelShader;

#define min(a, b) (a < b ? a : b)
#define max(a, b) (a > b ? a : b)

namespace phx
{

Model::Model() : 
	mDrawOutline(false),
	mCamera(nullptr),
	mModelMatrix(),
	mScale(1.0f, 1.0f, 1.0f),
	mMin(1e10f, 1e10f, 1e10f),
	mMax(-1e10f, -1e10f, -1e10f)
{
	static ModelShader __modelShader;
	modelShader = &__modelShader;
}

bool Model::loadFromFile(const std::string &filepath)
{
	Assimp::Importer importer;
	const aiScene *scene = importer.ReadFile(filepath.c_str(), aiProcessPreset_TargetRealtime_Quality);
	if(!scene)
	{
		#ifdef PHX_DEBUG
			std::cout<<importer.GetErrorString()<<std::endl;
		#endif
		return false;
	}
	DEBUG_SECTION
	(
		std::cout << "Loaded model data from " << filepath.c_str() << std::endl;
		std::cout<<scene->mNumAnimations<<" animations"<<std::endl;
		std::cout<<scene->mNumCameras<<" cameras"<<std::endl;
		std::cout<<scene->mNumLights<<" lights"<<std::endl;
		std::cout<<scene->mNumMaterials<<" materials"<<std::endl;
		std::cout<<scene->mNumMeshes<<" meshes"<<std::endl;
		std::cout<<scene->mNumTextures<<" textures"<<std::endl;
	)

	for(unsigned int i = 0; i < scene->mNumMaterials; ++i)
	{
		int texIndex = 0;
		aiString path; // texture filename

		aiReturn texFound = scene->mMaterials[i]->GetTexture(aiTextureType_DIFFUSE, texIndex, &path);
		while(texFound == AI_SUCCESS)
		{
			std::string _path = path.C_Str();
			if(_path[0] == '\\' || _path[0] == '/')
				_path.erase(_path.begin());
			for(size_t si = 0; si < _path.size(); ++si)
			{
				if(_path[si] == '/')
					_path[si] = '\\';
			}
			//std::transform(_path.begin(), _path.end(), _path.begin(), tolower);
			size_t found = filepath.find_last_of("/\\");
			if(found != std::string::npos)
				mTextureIdMap[filepath.substr(0, found+1) + _path] = nullptr;
			else
				mTextureIdMap[path.data] = nullptr;
			texIndex++;
			texFound = scene->mMaterials[i]->GetTexture(aiTextureType_DIFFUSE, texIndex, &path);
		}
	}

	std::map<std::string, gl::Texture*>::iterator it = mTextureIdMap.begin();
	for(; it != mTextureIdMap.end(); ++it)
	{
		const std::string &filename = it->first;

		gl::Texture *texture = gl::Texture::loadFromFile(filename);
		it->second = texture;
	}

	for(unsigned int m = 0; m < scene->mNumMeshes; m++)
	{
		aiMesh *mesh = scene->mMeshes[m];
		gl::ArrayObject *arrayObject = new gl::ArrayObject();
		if(mesh->HasFaces())
		{
			std::vector<unsigned int> faces(mesh->mNumFaces * 3);
			unsigned int faceIndex = 0;
			for(unsigned int i = 0; i < mesh->mNumFaces; i++)
			{
				const aiFace &face = mesh->mFaces[i];
				memcpy(&faces[faceIndex], face.mIndices, 3 * sizeof(unsigned int));
				faceIndex += 3;
			}
			arrayObject->setIndices(faces);
			//debugstring("Mesh %d contains %d indices\n", m, mesh->mNumFaces);
		}
		//else
			//debugstring("Mesh %d doesn't contain any indices\n", m);

		if(mesh->HasPositions())
		{
			for(unsigned int i = 0; i < mesh->mNumVertices; ++i)
			{
				aiVector3D pos = mesh->mVertices[i];
				mMin.x = min(mMin.x, pos.x);
				mMin.y = min(mMin.y, pos.y);
				mMin.z = min(mMin.z, pos.z);

				mMax.x = max(mMax.x, pos.x);
				mMax.y = max(mMax.y, pos.y);
				mMax.z = max(mMax.z, pos.z);
			}
			arrayObject->setVertices(mesh->mVertices, mesh->mNumVertices * 3);
			//debugstring("Mesh %d contains %d vertices\n", m, mesh->mNumVertices);
		}
		//else
			//debugstring("Mesh %d doesn't contain any vertices\n", m);

		if(mesh->HasNormals())
		{
			arrayObject->setNormals(mesh->mNormals, mesh->mNumVertices * 3);
			//debugstring("Mesh %d contains %d normals\n", m, mesh->mNumVertices);
		}
		//else If mesh doesn't contain any normals, then maybe generate them manually with cross product of vertices?
			//debugstring("Mesh %d doesn't contain any normals\n", m);

		if(mesh->HasTextureCoords(0))
		{
			std::vector<float> texCoords(mesh->mNumVertices * 2);
			for(unsigned int k = 0; k < mesh->mNumVertices; ++k)
			{
				texCoords[k * 2] = mesh->mTextureCoords[0][k].x;
				texCoords[k * 2 + 1] = mesh->mTextureCoords[0][k].y;
			}
			arrayObject->setTexCoords(texCoords);
			//debugstring("Mesh %d contains %d tex coords\n", m, mesh->mNumVertices);
		}
		//else
			//debugstring("Mesh %d doesn't contain any tex coords\n", m);
		/*
		if(mesh->HasBones())
		{
			unsigned int numBones = 0;
			for(unsigned int i = 0; i < mesh->mNumBones; ++i)
			{
				aiBone *bone = mesh->mBones[i];
				unsigned int boneIndex = 0;
				std::string boneName(bone->mName.data);

				std::map<std::string, unsigned int>::iterator it = mBoneMapping.find(boneName);
				if(it == mBoneMapping.end())
				{
					boneIndex = numBones;
					numBones++;

				}
			}
		}*/
		mArrayObjects.push_back(arrayObject);

		aiMaterial *mtl = scene->mMaterials[mesh->mMaterialIndex];
		aiString texPath;
		gl::Texture *texture = nullptr;
		int texCount = 0;
		if(mtl->GetTexture(aiTextureType_DIFFUSE, 0, &texPath) == AI_SUCCESS)
		{
			std::string _path = texPath.C_Str();
			if(_path[0] == '\\' || _path[0] == '/')
				_path.erase(_path.begin());
			for(size_t si = 0; si < _path.size(); ++si)
			{
				if(_path[si] == '/')
					_path[si] = '\\';
			}
			//std::transform(_path.begin(), _path.end(), _path.begin(), tolower);
			size_t found = filepath.find_last_of("/\\");
			if(found != std::string::npos)
				_path = filepath.substr(0, found+1) + _path;

			std::map<std::string, gl::Texture*>::iterator it = mTextureIdMap.find(_path);
			if(it != mTextureIdMap.end())
				texture = mTextureIdMap[_path];
			if(texture)
				printf("Using texture %s\n", _path.c_str());
			else
				printf("Texture is NULL: %s\n", _path.c_str());
		}
		else
			printf("Not using textures for model: %s\n", filepath.c_str());
		if(texture)
			texCount = 1;
		mTextures.push_back(texture);

		gl::Material material(mtl, texCount);
		gl::UniformBuffer *materialBuffer = new gl::UniformBuffer();
		materialBuffer->setData((void*)&material, sizeof(material));
		mMaterialBuffers.push_back(materialBuffer);
	}

	//mMin.x = -abs(mMin.x); mMin.y = -abs(mMin.y); mMin.z = -abs(mMin.z);
	//mMax.x = abs(mMax.x); mMax.y = abs(mMax.y); mMax.z = abs(mMax.z);
	float tmp = abs(mMax.x) - -abs(mMin.x);
	tmp = max(tmp, abs(mMax.y) - -abs(mMin.y));
	tmp = max(tmp, abs(mMax.z) - -abs(mMin.z));
	float scaleFactor = 1.0f / tmp;
	mScale.x = mScale.y = mScale.z = scaleFactor;

	mMatricesBuffer.setData(nullptr, 3 * 16 * sizeof(float), gl::UniformBuffer::DYNAMIC);
	
	glm::mat4 view = glm::lookAt(
		glm::vec3(0.0f, 0.0f, -50.0f),
		glm::vec3(0.0f, 0.0f, 0.0f),
		glm::vec3(0.0f, 1.0f, 0.0f)
	);
	mMatricesBuffer.setSubData(glm::value_ptr(view), sizeof(view), sizeof(float) * 16);

	glm::mat4 proj = glm::perspective(45.0f, 1280.0f / 768.0f, 0.001f, 1000.0f);
	mMatricesBuffer.setSubData(glm::value_ptr(proj), sizeof(proj), 0);

	mModelMatrix = glm::scale(mModelMatrix, glm::vec3(mScale.x, mScale.y, mScale.z));
	mMatricesBuffer.setSubData(glm::value_ptr(mModelMatrix), sizeof(mModelMatrix), sizeof(float) * 16 * 2);
	
	//phx::vec3f newPos(-(mMax.x - mMin.x)*0.5f*scaleFactor, -(mMax.y - mMin.y)*0.5f*scaleFactor, -(mMax.z - mMin.z)*0.5f*scaleFactor);
	//newPos.x = 0.0f; newPos.z = 0.0f;
	//setPosition(newPos);
	//printf("New position: %f, %f, %f\n", newPos.x, newPos.y, newPos.z);
	//printf("Min y: %f\n", mMin.y);

	//modelShader->matricesBlock.setBuffer(mMatricesBuffer);

	return true;
}

void Model::render()
{
	glEnable(GL_DEPTH_TEST);
	//glEnable(GL_CULL_FACE);
	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	modelShader->matricesBlock.setBuffer(mMatricesBuffer);
	if(mCamera)
		mMatricesBuffer.setSubData(glm::value_ptr(mCamera->mTransformation), sizeof(mCamera->mTransformation), sizeof(float) * 16);
	modelShader->shaderObject.use();

	int numMeshes = mArrayObjects.size();
	for(int i = 0; i < numMeshes; ++i)
	{
		modelShader->materialBlock.setBuffer(*mMaterialBuffers[i]);
		if(mTextures[i])
		{
			mTextures[i]->use();
			//modelShader->shaderObject.setValue("texUnit", mTextures[i]->getUnit());
		}
		mArrayObjects[i]->render();
	}
	//printGLerror();
	/*
	glEnable(GL_STENCIL_TEST);
	glStencilFunc(GL_ALWAYS, 1, 0xFFFF);
	glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	GLint colzPos = glGetUniformLocation(mShaderProgram, "colz");
	glUniform3f(colzPos, 0.0f, 0.0f, 0.0f);
	glDrawArrays(GL_TRIANGLES, 0, mNumTriangles*3*sizeof(float));

	glStencilFunc(GL_NOTEQUAL, 1, 0xFFFF);
	glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
	glLineWidth(0.5f);
	glEnable(GL_LINE_SMOOTH);
	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	glUniform3f(colzPos, 1.0f, 1.0f, 1.0f);
	glDrawArrays(GL_TRIANGLES, 0, mNumTriangles*3*sizeof(float));
	*/
	/*
	if(mDrawOutline)
	{
		float outlineWidth = 2.0f;

		glEnable(GL_POLYGON_OFFSET_FILL);
		glPolygonOffset(-outlineWidth-2.0f, -outlineWidth-2.0f);
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
		glLineWidth(outlineWidth);
		glDrawArrays(GL_TRIANGLES, 0, mNumTriangles * 3 * sizeof(float));

		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

		glDrawArrays(GL_TRIANGLES, 0, mNumTriangles * 3 * sizeof(float));

		glDisable(GL_POLYGON_OFFSET_FILL);
		glPolygonOffset(0.0f, 0.0f);
	}
	else
	{
		glDrawArrays(GL_TRIANGLES, 0, mNumTriangles * 3 * sizeof(float));
	}
	*/
}

bool Model::getOutlinesVisible()
{
	return mDrawOutline;
}

void Model::setOutlinesVisible(bool visible)
{
	mDrawOutline = visible;
}

void Model::setCamera(phx::Camera &camera)
{
	mCamera = &camera;
}

void Model::setPosition(const phx::vec3f &pos)
{
	mModelMatrix[3][0] = pos.x;
	mModelMatrix[3][1] = pos.y;
	mModelMatrix[3][2] = pos.z;

	mMatricesBuffer.setSubData(glm::value_ptr(mModelMatrix), sizeof(mModelMatrix), sizeof(float) * 16 * 2);
}

void Model::setScale(const phx::vec3f &scale)
{
	mScale = scale;
	mModelMatrix = glm::scale(mModelMatrix, glm::vec3(mScale.x, mScale.y, mScale.z));
	mMatricesBuffer.setSubData(glm::value_ptr(mModelMatrix), sizeof(mModelMatrix), sizeof(float) * 16 * 2);
}

void Model::rotate(const float degrees, const phx::vec3f &angle)
{
	mModelMatrix = glm::rotate(mModelMatrix, degrees, glm::vec3(angle.x, angle.y, angle.z));
	mMatricesBuffer.setSubData(glm::value_ptr(mModelMatrix), sizeof(mModelMatrix), sizeof(float) * 16 * 2);
}

Model::~Model()
{
	while(!mArrayObjects.empty())
	{
		delete mArrayObjects.back();
		mArrayObjects.pop_back();
	}

	while(!mMaterialBuffers.empty())
	{
		delete mMaterialBuffers.back();
		mMaterialBuffers.pop_back();
	}

	for(std::map<std::string, gl::Texture*>::iterator it = mTextureIdMap.begin(); it != mTextureIdMap.end();)
	{
		delete it->second;
		it = mTextureIdMap.erase(it);
	}
}

}